package jannex.sql;

import jannex.Builder;
import jannex.Jannex;
import jannex.JannexException;
import jannex.internal.CloseUtils;
import jannex.label.LabelMap;
import jannex.mapping.MappedField;
import jannex.mapping.Mapping;
import jannex.mapping.ResultSetHandler;
import jannex.sql.extensions.ResultSetImpl;
import jannex.sql.extensions.TypedResultSetImpl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Query {

    protected final Jannex jannex;

    private final Builder<JxConnection> connectionBuilder;
    private final String sql;
    private final String originalSql;

    private int resultSetType = JxResultSet.TYPE_FORWARD_ONLY;
    private int resultSetConcurrency = JxResultSet.CONCUR_READ_ONLY;
    private int resultSetHoldability = Integer.MIN_VALUE;
    private int fetchDirection = JxResultSet.FETCH_FORWARD;

    private int fetchSize = 0;
    private int maxRows = 0;
    private boolean poolable = true;
    private int queryTimeout = 0;

    private List<Object> arguments = new ArrayList<>();
    private Map<String, Integer> map;

    public Query(final JxConnection connection, String sql) {
        this.connectionBuilder = new Builder<JxConnection>() {
            @Override
            public JxConnection build() {
                return connection;
            }
        };
        this.jannex = Jannex.STANDALONE;
        this.originalSql = sql;
        this.sql = parse(sql);
    }

    public Query(Builder<JxConnection> connectionBuilder, Jannex jannex, String sql) {
        this.connectionBuilder = connectionBuilder;
        this.jannex = jannex;
        this.originalSql = sql;
        this.sql = parse(sql);
    }

    protected Query(Query query) {
        this.jannex = query.jannex;
        this.connectionBuilder = query.connectionBuilder;
        this.sql = query.sql;
        this.originalSql = query.originalSql;
        this.resultSetType = query.resultSetType;
        this.resultSetConcurrency = query.resultSetConcurrency;
        this.resultSetHoldability = query.resultSetHoldability;
        this.fetchSize = query.fetchSize;
        this.maxRows = query.maxRows;
        this.arguments = query.arguments;
        this.map = query.map;
    }

    public Query set(Object object) {
        if (arguments == null) {
            arguments = new ArrayList<>();
        }
        arguments.add(object);
        return this;
    }

    @SuppressWarnings("unchecked")
    public Query setFrom(Object object) {
        if (object instanceof List) {
            return setAll((List<Object>) object);
        } else if (object instanceof Map) {
            return setAll((Map<String, Object>) object);
        }
        Mapping<?> mapping = jannex.getMapping(object.getClass());
        LabelMap.Builder<Object> parameters = LabelMap.builder();
        setFromHelper(object, mapping.getFields(), parameters);
        return setAll(parameters.build());
    }

    private void setFromHelper(Object object, MappedField<?>[] fields, LabelMap.Builder<Object> parameters) {
        for (MappedField<?> mappedField : fields) {
            if (mappedField.getFields() == null) {
                @SuppressWarnings("unchecked")
                MappedField<Object> castField = (MappedField<Object>) mappedField;
                Object fieldObject = castField.getGetter().get(object);
                parameters.put(mappedField.getLabel(), fieldObject);
            } else {
                setFromHelper(object, mappedField.getFields(), parameters);
            }
        }
    }

    public Query set(String key, Object object) {
        if (map == null) {
            throw new JannexException("No colon expressions found in sql statment: " + originalSql);
        }
        Integer index = map.get(key);
        if (index == null) {
            throw new JannexException("Key '" + key + "' not found in query: " + originalSql);
        }
        while (arguments.size() <= index) {
            arguments.add(null);
        }
        arguments.set(index, object);
        return this;
    }

    public Query setAll(Iterable<Object> list) {
        for (Object o : list) {
            set(o);
        }
        return this;
    }

    public Query setAll(Object... objects) {
        return setAll(Arrays.asList(objects));
    }

    public Query setAll(Map<String, Object> map) {
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            set(entry.getKey(), entry.getValue());
        }
        return this;
    }

    public <E> TypedQuery<E> as(Class<E> elementClass) {
        return as(jannex.getHandler(elementClass));
    }

    public <E> TypedQuery<E> as(ResultSetHandler<E> handler) {
        return new TypedQuery<E>(this, handler);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        int index = 0;
        for (int i = 0; i < sql.length(); i++) {
            char c = sql.charAt(i);
            if (c == '?' && index < arguments.size()) {
                Object obj = arguments.get(index++);
                sb.append(fieldToString(obj));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    private String fieldToString(Object obj) {
        // TODO Time, TimeStamp, Date
        if (obj == null) {
            return "NULL";
        } else if (obj.getClass().isArray()) {
            StringBuilder sb = new StringBuilder();
            Object[] array = (Object[]) obj;
            sb.append("ARRAY[");
            for (int j = 0; j < array.length; j++) {
                sb.append(fieldToString(array[j]));
                if (j != array.length-1) {
                    sb.append(',');
                }
            }
            return sb.append(']').toString();
        } else if (obj instanceof String) {
            return "'" + obj + "'";
        } else {
            return obj.toString();
        }
    }

    public <E> Fetcher<E> fetch(Class<E> elementClass, Object... arguments) {
        return fetch(jannex.getHandler(elementClass), arguments);
    }

    public <E> Fetcher<E> fetch(final ResultSetHandler<E> handler, final Object... arguments) {
        JxPreparedStatement statement = null;
        try {
            statement = build(arguments);
            final JxPreparedStatement finalStatement = statement;
            Builder<TypedResultSet<E>> builder = new Builder<TypedResultSet<E>>() {
                @Override
                public TypedResultSet<E> build() {
                    try {
                        return new TypedResultSetImpl<E>(finalStatement.executeQuery(), jannex, handler) {
                            @Override
                            public void close() {
                                CloseUtils.closeAll(finalStatement, resultSet);
                            }
                        };
                    } catch (RuntimeException e) {
                        throw CloseUtils.close(finalStatement, e);
                    }
                }
            };
            return new Fetcher<>(builder);
        } catch (RuntimeException e) {
            throw CloseUtils.close(statement, e);
        }
    }

    public int execute(Object... arguments) {
        try (JxPreparedStatement statement = build(arguments)) {
            return statement.executeUpdate();
        }
    }

    public JxPreparedStatement build(Object... arguments) {

        if (arguments.length != 0) {
            setAll(arguments);
        }

        JxConnection connection = null;
        JxPreparedStatement preparedStatement = null;

        try {
            connection = connectionBuilder.build();
            if (resultSetHoldability != Integer.MIN_VALUE) {
                preparedStatement = connection
                        .prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
            } else {
                preparedStatement = connection
                        .prepareStatement(sql, resultSetType, resultSetConcurrency);
            }

            preparedStatement.setFetchSize(fetchSize);
            preparedStatement.setMaxRows(maxRows);
            preparedStatement.setPoolable(poolable);
            preparedStatement.setQueryTimeout(queryTimeout);
            preparedStatement.setFetchDirection(fetchDirection);

            for (int i = 0; i < preparedStatement.getParameterMetaData().getParameterCount(); i++) {
                Object o = i < this.arguments.size() ? this.arguments.get(i) : null;
                preparedStatement.setObject(i+1, o);
            }

            return preparedStatement;
        } catch (RuntimeException e) {
            try {
                CloseUtils.closeAll(preparedStatement, connection);
            } catch (Exception suppressed) {
                e.addSuppressed(suppressed);
            }
            throw e;
        }
    }

    public BatchExecutor batch() {
        return new BatchExecutor(this);
    }

    public JxResultSet resultSet(Object... arguments) {
        JxPreparedStatement statement = null;
        JxResultSet resultSet = null;
        try {
            statement = build(arguments);
            resultSet = statement.executeQuery();
            final JxPreparedStatement finalStatement = statement;
            final JxResultSet finalResultSet = resultSet;
            return new ResultSetImpl(finalResultSet, jannex) {
                @Override
                public void close() {
                    CloseUtils.closeAll(finalResultSet, finalStatement);
                }
            };
        } catch (Exception e) {
            CloseUtils.closeAll(statement, resultSet);
            throw e;
        }
    }

    private String parse(String sql) {
        StringBuilder sqlSb = new StringBuilder();
        int index = 0;
        for (int i = 0; i < sql.length(); i++) {
            char c = sql.charAt(i);
            if (c == ':' && (i == 0 || sql.charAt(i - 1) != '\\')) {
                StringBuilder variable = new StringBuilder();
                for (i++; i < sql.length(); i++) {
                    char o = sql.charAt(i);
                    if (!Character.isDigit(o) && !Character.isAlphabetic(o) && o != '_') {
                        i--;
                        break;
                    }
                    variable.append(o);
                }
                if (map == null) {
                    map = new HashMap<>();
                }
                map.put(variable.toString(), index++);
                sqlSb.append('?');
            } else if (c == ':' && sql.charAt(i - 1) == '\\') {
                // Remove escaping \
                sqlSb.deleteCharAt(i - 1);
                sqlSb.append(c);
            } else {
                sqlSb.append(c);
            }
        }
        return sqlSb.toString();
    }

    public String getSql() {
        return sql;
    }

    public String getOriginalSql() {
        return originalSql;
    }

    public Query setResultSetType(int resultSetType) {
        this.resultSetType = resultSetType;
        return this;
    }

    public Query setResultSetConcurrency(int resultSetConcurrency) {
        this.resultSetConcurrency = resultSetConcurrency;
        return this;
    }

    public Query setResultSetHoldability(int resultSetHoldability) {
        this.resultSetHoldability = resultSetHoldability;
        return this;
    }

    public Query setFetchSize(int fetchSize) {
        this.fetchSize = fetchSize;
        return this;
    }

    public Query setMaxRows(int maxRows) {
        this.maxRows = maxRows;
        return this;
    }

    public Query setFetchDirection(int fetchDirection) {
        this.fetchDirection = fetchDirection;
        return this;
    }

    public Query setPoolable(boolean poolable) {
        this.poolable = poolable;
        return this;
    }

    public Query setQueryTimeout(int queryTimeout) {
        this.queryTimeout = queryTimeout;
        return this;
    }

}
